﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Windows.Input;
using Microsoft.WindowsAPICodePack.Dialogs;
using System.Windows.Interop;
using System.Windows;
using System.Timers;
using System.Windows.Threading;
using MineSweeper.Themes;
using SwiftMVVM.ViewModel;
using MineSweeper.Behaviors;

namespace MineSweeper.Data
{
    /// <summary>
    /// Represents a tile map 
    /// </summary>
    public abstract class TileMap : DependentBindingObject
    {
        /// <summary>
        /// Flag we're using to determine if the game is in
        /// a new state or if its restarted
        /// </summary>
        public bool IsNewGame { get; private set; }

        /// <summary>
        /// The size of the tile including it's border
        /// </summary>
        private const int BorderedTileSize = 18;

        /// <summary>
        /// The width of the tile, plus surrounding grid
        /// </summary>
        [DependsOn("Width")]
        public int TileMapActualWidth { get { return ((Width) * BorderedTileSize) + 2 ; } }

        /// <summary>
        /// Height of the tile, plus surrounding grid
        /// </summary>
        [DependsOn("Height")]
        public int TileMapActualHeight { get { return ((Height) * BorderedTileSize) + 2; } }

        /// <summary>
        /// The tile with input focus
        /// </summary>
        [NotificationProperty]
        public virtual Tile FocusedTile { get; set; }

        /// <summary>
        /// The width of the board
        /// </summary>
        [NotificationProperty]
        public virtual int Width { get; set; }

        /// <summary>
        /// The height of the board
        /// </summary>
        [NotificationProperty]
        public virtual int Height { get; set; }

        /// <summary>
        /// The count of mines on the board
        /// </summary>
        [NotificationProperty]
        public virtual int Mines { get; set; }

        /// <summary>
        /// Collection of tiles on the map (plus hit test padding)
        /// </summary>
        [NotificationProperty]
        public virtual ObservableCollection<Tile> AllTiles { get; set; }

        /// <summary>
        /// All of the active (non-noop) tiles on the board
        /// </summary>
        [DependsOn("AllTiles")]
        public IEnumerable<Tile> Tiles { get { return this.AllTiles.Where(tile => tile.Type != TileType.NoOp); } }

        /// <summary>
        /// The timer that times the length of the game.
        /// </summary>
        public readonly DispatcherTimer dispatcherTimer = new DispatcherTimer();

        /// <summary>
        /// The number of seconds that the game has been going on for.
        /// The timer starts when the first tile is clicked.
        /// </summary>
        [NotificationProperty]
        public virtual int Seconds { get; set; }
       
        /// <summary>
        /// Creates an instance of the tilemap proxy
        /// </summary>
        /// <returns></returns>
        public static TileMap Create()
        {
            return NotifyProxyTypeManager.Allocate<TileMap>();
        }


        public TileMap()
        {
            this.AllTiles = new ObservableCollection<Tile>();
            this.UseChangeMonitor = true;
            this.dispatcherTimer.Interval = new TimeSpan(0, 0, 1);
            this.dispatcherTimer.Tick += (s, e) => { this.Seconds++; };

            //Must initialize settings for best times arraylists
            if (Properties.Settings.Default.StatBestTimesBeg == null)
            {
                Properties.Settings.Default.StatBestTimesBeg = new System.Collections.ArrayList();
            }
            if (Properties.Settings.Default.StatBestTimesInt == null)
            {
                Properties.Settings.Default.StatBestTimesInt = new System.Collections.ArrayList();
            }
            if (Properties.Settings.Default.StatBestTimesAdv == null)
            {
                Properties.Settings.Default.StatBestTimesAdv = new System.Collections.ArrayList();
            }

            Properties.Settings.Default.SettingsModified += (sender, eventArgs) => 
            {
                if (IsNewGame || Environment.OSVersion.Version.Major < 6)
                {
                    NewGame();
                }
                else
                {
                    using (var enableThemingInScope = new EnableThemingInScope(true))
                    {
                        using (var taskDialog = new TaskDialog())
                        {
                            taskDialog.Caption = "Changed Game Settings";
                            taskDialog.InstructionText = "These settings won't apply to the current game. What do you want to do?";
                            taskDialog.Cancelable = true;
                            var windowInteropHelper = new WindowInteropHelper(App.Current.MainWindow);
                            taskDialog.OwnerWindowHandle = windowInteropHelper.Handle;
                            var closeDialog = new Action<object>((s) =>
                            {
                                ((TaskDialog)((TaskDialogCommandLink)s).HostingDialog).Close();
                            });
                            var quit = new TaskDialogCommandLink(
                                "QuitCommandLink",
                                "Quit and start a new game with new settings",
                                "This counts as a loss in your statistics.");
                            quit.Click += ((s, e) =>
                            {
                                closeDialog(s);
                                NewGame();
                            });

                            var finish = new TaskDialogCommandLink(
                                "RestartCommandLink",
                                "Finish the game",
                                "The new settings apply to your next game.");
                            finish.Click += ((s, e) =>
                            {
                                closeDialog(s);
                            });

                            taskDialog.Controls.Add(quit);
                            taskDialog.Controls.Add(finish);

                            taskDialog.Show();
                        }
                    }
                }
            };

            NewGame();
        }


        /// <summary>
        /// The count of remaining mines as perceived by the player
        /// </summary>
        [NotificationProperty]
        public virtual int PerceivedRemainingMines { get; set; }

        /// <summary>
        /// Returns the count of mines that are flagged
        /// </summary>
        [DependsOn("AllTiles/Item", "State")]
        public int FlaggedMines
        {
            get
            {
                var flaggedTiles = this.Tiles.Where(tile => tile.State == TileState.Flagged).ToList();
                PerceivedRemainingMines = this.Mines - flaggedTiles.Count;
                return flaggedTiles.Count(tile => tile.Type == TileType.Bomb);
            }
        }

        /// <summary>
        /// Creates a new game with a new mine layout based
        /// on the minecount/and dimensions of the board
        /// </summary>
        public void NewGame()
        {
            switch ((GameDifficulty)Properties.Settings.Default.GameOptionDifficultyInt)
            {
                case GameDifficulty.Beginner:
                    this.Mines = 10;
                    this.Width = 9;
                    this.Height = 9;
                    break;
                case GameDifficulty.Intermediate:
                    this.Mines = 40;
                    this.Width = 16;
                    this.Height = 16;
                    break;
                case GameDifficulty.Advanced:
                    this.Mines = 99;
                    this.Width = 30;
                    this.Height = 16;
                    break;
                case GameDifficulty.Custom:
                    this.Mines = Properties.Settings.Default.GameOptionMines;
                    this.Width = Properties.Settings.Default.GameOptionWidth;
                    this.Height = Properties.Settings.Default.GameOptionHeight;
                    break;
                default:
                    throw new InvalidOperationException("Invalid game difficulty selected");
            }

            this.IsNewGame = true;
            this.FocusedTile = null;

            //In order to optimize surrounding mine detection we will pad
            //the entire board by 1 no-op tile
            var actualHeight = Height + 2;
            var actualWidth = Width + 2;

            this.AllTiles.Clear();
            OnPropertyChanging("Tiles");
            Enumerable.Range(0, actualHeight * actualWidth).Select(
                index => Tile.Create(this, index,
                    index <= actualWidth ||
                    (index % actualWidth == 0 || (index + 1) % actualWidth == 0) ||
                    index >= actualWidth * (Height + 1) ? TileType.NoOp :
                    TileType.Safe)).ToList().ForEach(t => this.AllTiles.Add(t));
            OnPropertyChanged("Tiles");

            this.Seconds = 0;
        }

        /// <summary>
        /// Resets the current board by recovering all of the
        /// uncovered tiles
        /// </summary>
        public void ResetGame()
        {
            foreach (Tile tile in this.Tiles)
            {
                tile.State = TileState.Covered;
            }
            this.Seconds = 0;
        }

        /// <summary>
        /// Uncovers the given tile, and in some cases the surrounding tiles
        /// </summary>
        /// <param name="tile">The tile we're going to uncover</param>
        public void UncoverTile(Tile tile)
        {
            if (!this.dispatcherTimer.IsEnabled)
            {
                this.Seconds = 0;
                this.dispatcherTimer.IsEnabled = true;
            }

            if (tile.Type == TileType.Bomb)
            {
                this.dispatcherTimer.IsEnabled = false;
                foreach (var bombTile in tile.Parent.AllTiles.Where(t => t.Type == TileType.Bomb))
                {
                    bombTile.State = TileState.Uncovered;
                }

                //And we're done, notify the user
                if (Environment.OSVersion.Version.Major >= 6)
                {
                    using (var enableThemingInScope = new EnableThemingInScope(true))
                    {
                        using (var taskDialog = new TaskDialog())
                        {
                            Commands.GameCommands.RecordStats(false);
                            taskDialog.Caption = "Sorry, you lost this game. Better luck next time!\n\n";
                            taskDialog.InstructionText = "Game Lost";

                            taskDialog.Cancelable = true;
                            var windowInteropHelper = new WindowInteropHelper(App.Current.MainWindow);
                            taskDialog.OwnerWindowHandle = windowInteropHelper.Handle;
                            var closeDialog = new Action<object, TaskDialogResult>((s, tdr) =>
                            {
                                ((TaskDialog)((TaskDialogButton)s).HostingDialog).Close(tdr);
                            });

                            var quit = new TaskDialogButton(
                                "ExitButton",
                                "Exit");
                            quit.Click += ((s, e) =>
                            {
                                closeDialog(s, TaskDialogResult.No);
                                App.Current.MainWindow.Close();
                            });

                            var restart = new TaskDialogButton(
                                "RestartCommandLink",
                                "Restart this game");
                            restart.Click += ((s, e) =>
                            {
                                closeDialog(s, TaskDialogResult.Ok);
                                this.ResetGame();
                            });

                            var playAgain = new TaskDialogButton(
                                "PlayAgainCommandLink",
                                "Play Again");
                            playAgain.Default = true;
                            playAgain.Click += ((s, e) =>
                            {
                                closeDialog(s, TaskDialogResult.Ok);
                                this.NewGame();
                            });

                            taskDialog.Controls.Add(quit);
                            taskDialog.Controls.Add(restart);
                            taskDialog.Controls.Add(playAgain);

                            if (taskDialog.Show() == TaskDialogResult.Cancel)
                            {
                                this.NewGame();
                            }
                        }
                    }
                }
                else
                {
                    //Provide a fallback implementation for windows XP
                    if (MessageBox.Show("Sorry, you lost this game. Better luck next time! Play again?", 
                        "Game Lost",
                        MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes)
                    {
                        this.NewGame();
                    }
                    else
                    {
                        App.Current.MainWindow.Close();
                    }
                }
            }
            else
            {
                //Have we uncovered any tiles yet?
                if (IsNewGame)
                {
                    //We only set the mines after the first tile has been uncovered
                    //because we do not want the first uncover to be a mine
                    IsNewGame = false;
                    var actualHeight = Height + 2;
                    var actualWidth = Width + 2;
                    var tileCount = (Height + 2) * (Width + 2);
                    var tileIndex = tile.Parent.AllTiles.IndexOf(tile);

                    var mineTiles = Enumerable.Range(0, tileCount).OrderBy(n => Guid.NewGuid())
                        .Where(potentialMineIndex =>
                            potentialMineIndex != tileIndex &&
                            potentialMineIndex > actualWidth &&
                            !(potentialMineIndex % actualWidth == 0 || (potentialMineIndex + 1) % actualWidth == 0) &&
                            potentialMineIndex < actualWidth * (Height - 1)).Take(Mines).ToList().
                            Select(mi =>
                                {
                                    var mineTile = this.AllTiles[mi];
                                    mineTile.Type = TileType.Bomb;
                                    return mineTile;
                                }).ToList();

                    foreach (var distinctAdjacentTile in mineTiles.SelectMany(c => c.AdjacentTiles.Where(t=>t.Type != TileType.NoOp)).Distinct())
                    {
                        distinctAdjacentTile.NearbyMineCount = distinctAdjacentTile.AdjacentTiles.Count(
                               t => t.Type == TileType.Bomb);
                    }
                }

                //Uncover the tile
                var queue = new Queue<Tile>();
                if (tile.State != TileState.Uncovered)
                {
                    tile.State = TileState.Uncovered;
                    queue.Enqueue(tile);
                }

                //Now check the surrounding tiles
                while (queue.Count > 0)
                {
                    var queuedTile = queue.Dequeue();
                    if (queuedTile.NearbyMineCount == 0 && queuedTile.Type != TileType.NoOp)
                    {
                        foreach (var adjacentTile in queuedTile.AdjacentTiles)
                        {
                            if (adjacentTile.Type != TileType.NoOp &&
                                (adjacentTile.State == TileState.Covered ||
                                adjacentTile.State == TileState.Question))
                            {
                                adjacentTile.State = TileState.Uncovered;
                                queue.Enqueue(adjacentTile);
                            }
                        }
                    }
                }
            }
        }
    }
}
